#!/usr/bin/perl -w

# $Id: foo.pl 7557b5d4d82b 2012/12/02 09:07:16 pgurumur $
# Copyright (c) 2009 Prabhu Gurumurthy <pgurumur@gmail.com>
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#       

use strict;
use warnings;
use Digest::SHA;
use Getopt::Long;
use File::Copy;
use Mail::Sendmail 0.75;
use POSIX;
use POE;
use Proc::Daemon;
use Sys::Syslog;
use Sys::Syslog qw(:DEFAULT setlogsock);
# use lib "/usr/ssn/lib/perl5/";
use ConfigCopy;
use ConfigSQL;
use ConfigRCS;

# Configure getopt, as it says!
Getopt::Long::Configure ('default', 'bundling');

# Similar to basename
push my @prog, split(/\//, $0);
my $program = $prog[$#prog];

# See whether we have any arguments, if not, print usage and exit.
my $arg = scalar(@ARGV);

# where I store my command line options
my %opt = ();

# MySQL handle
my $sql = undef;

# Sleep time before the device backs itself up!
# Default is 20 seconds, tunable with -s or --sleep option
my $sleep = undef;

# Where is my backup directory?
my $backup_dir = "/tftpboot/bckup";

# Where are my rcs, co, ci, rcsdiff binaries located?
my $bindir = "/usr/bin";

# How many number of tasks
my $taskMax = 5;

# My RCS handle 
my $rcs = undef; 

# Default is we do not enable debug
my $debug = 0;

# usage function, prints what this can take as command line options and exits
sub usage {
   print "$program options\n";
   print "options: \n";
   print " -d | --debug       [ Do not run in daemon mode ]\n";
   print " -C | --conf        [ Database configuration file ]\n";
   print " -s | --sleep       [ Sleep time for system to back itself up ]\n";
   print " -t | --task        [ maximum concurrent task, default: 5 ]\n";
   print " -T | --tftp        [ specify the tftp server ]\n";
   print " -h | --hostname    [ hostname of the device ]\n";
   print " -c | --community   [ SNMP rw community string for the device ]\n";
   print " -u | --user        [ SSH username for the device ]\n";
   print " -p | --pass        [ SSH password for the username ]\n";
   print " -? | --help        [ prints this help ]\n";
   exit (1);
}

# Open SQL handle based on the self explaing parameters given.
sub sqlOpen {
   my ($hostname, $database, $username, $password) = @_;

   logging("hostname: $hostname");
   logging("database: $database");
   logging("username: $username");

   my $retval = 1;
   # I dont want to over ride an existing handle
   if (!defined($sql)) {
      if (defined($hostname) and defined($database) and defined($username) 
            and defined($password)) {
         $sql = new ConfigSQL($database, $hostname, $username, $password); 
         $retval = 0;
      }
   }

   logging("return value: $retval");
   return $retval;
}

# Close SQL handle if it is opened
sub sqlClose {
   
   if (defined($sql)) {
      $sql->close();
   }

}

# Do the sql query and return the array, the caller is responsible for
# getting the values from the array, not this function as I cannot determine
# what type of query is passed.
sub sqlSelect {
   my ($sqlQuery) = @_;

   logging("SQL Query: $sqlQuery");
   my @retval = ();
   if (defined($sqlQuery) and defined($sql)) {
      @retval = $sql->execute($sqlQuery);
   }

   return @retval;
}

# Function to use for Update and Insert
sub sqlInsert {
   my ($sqlQuery) = @_;

   logging("SQL Query: $sqlQuery");
   if (defined($sqlQuery) and defined($sql)) {
      $sql->insert($sqlQuery);
   }
}

# This is another wrapper, we only log, if debug flag is set to 1, in this way
# logging function can be strewn around, and nothing will get logged, unless
# debug flag is set.
sub logging {
   my ($message) = @_;

   if ($debug eq 1) {
      mesglog("debug", $message);
   }
}


# Wrapper function syslog
sub mesglog {
   my ($facility, $message) = @_;

   # If facility is not defined, we default to info
   if (defined($message)) {
      if (!defined($facility) or ($facility eq 0)) {
         $facility = "info";
      }

      syslog($facility, "%s\n", $message);
   }
}

# fatal, as the function says, prints the message and exists after closing the
# sql handle, if $email is defined and it is 1, then we send email out using
# email function (caveat, email itself uses fatal, but with $email eq 0)
# otherwise it will be a loop
sub fatal {
   my ($email, $message) = @_;

   my @msg = ();
   if (defined($message)) {
      if (defined($sql)) {
         sqlClose();
      }

      mesglog(0, "%s: %s", $program, $message);

      if (defined($email) and ($email eq 1)) {
         push @msg, $message;
         # XXX: POSIX::uname() is use in multiple places, dont know whether we 
         # can use it only once and reference the variable
         my ($sys, $node, $release, $ver, $machine) = POSIX::uname();
         my $contact = 'netreports@silverspringnet.com';
         my $subject = sprintf("%s encountered problem on %s", $program, $node);
         email($contact, $subject, @msg);
      }

      # if we opened syslog, we want to close it before we exit out!
      closelog;

      # fatal function always exits out return 1
      exit(1);
   }
}

# Send mail, (as the function suggests) 
sub email {
   my ($contact, $subject, @body) = @_;

   my %mail = undef;
   my ($sys, $node, $release, $ver, $machine) = POSIX::uname();

   if (defined($contact)) {
      my $from = 'netreports@silverspringnet.com';
      %mail = (
         from => $from,
         to => $contact,
         subject => $subject);

      logging("sending email to: $contact");

      $mail{body} = @body;
      sendmail(%mail) or fatal(0, "$Mail::Sendmail::error");
   }
}

sub sha {
   my ($file) = @_;

   my $digest = undef;
   if (defined($file) and -f $file) {
      my $ctx = Digest::SHA->new("sha1");
      $ctx->addfile($file);
      $digest = $ctx->hexdigest;
   }

   return $digest;
}


# Where the backup process is being done, in this way I am merging the perl 
# script and the shell script into one and it easier to debug for any perl
# monks (too bad shell guys, learn some perl)
sub rcs_backup {
   my ($system, $contact, $orig_conf) = @_;

   # Get the time in nice format instead of numbers
   my $now = localtime();

   logging("time: $now");

   # We default to root and oper group, used by chown down below
   # SSN specific
   my $uid = 0;
   my $gid = 2002;

   # Default is read for owner and group, and nothing for everybody else.
   my $mode = 0440;

   # My original config, gotten from get_config_file from ConfigCopy.pm
   # is actually in /tftpboot, but my backup_dir can be something else.
   # So, we check to see whether
   #     1. original config is present and it has a valid size
   #     2. Get the sysconfig file using orig_conf and backup dir as show below
   #     3. Check to see the system config file exists in the backup directory
   #     4. Do the RCS magic
   if (-f $orig_conf && -s $orig_conf) {
      push my @temp, split(/\//, $orig_conf);
      my $sys_conf = sprintf("%s/%s", $backup_dir, $temp[$#temp]);

      # Huh? If I dont have an original configuration (empty configuration is
      # fine), how would RCS proceed?
      if (! -f $sys_conf) {
         fatal(1, "cannot find the configuration file for $system");
      }

      logging("original system config: $sys_conf");
      logging("newly backup system config: $orig_conf");

      if (defined($rcs)) {
         # checkout the original configuration file, I don't care whether it
         # is empty or not empty
         $rcs->checkout($sys_conf);

         # Once checked out, move the new configuration file to original one
         move($orig_conf, $sys_conf);

         # Get the RCS diff, since we moved it right
         my @changed = $rcs->diff();

         # message for rcs checkin
         my $message = sprintf("automatic checkin of %s config file on %s", 
            $system, $now);

         logging("automatic rcs checkin message: $message");

         # Checkin the file
         $rcs->checkin($sys_conf, $message);

         # chown the file, so that only root and oper group people can use it
         # and chmod it with $mode
         chown $uid, $gid, $sys_conf;
         chmod $mode, $sys_conf;

         # We only send email out, the something has changed, no point in
         # spamming the receiver if nothing changed.
         if (@changed) {

            # Get the digest and update the DB
            my $digest = sha($sys_conf);
            logging("SHA1 digest: $digest");

            if (defined($digest)) {
               # Update the timestamp and MD5 sum of when we did the backup on 
               # the SQL table
               my $sql_query = "update config_backup set timestamp = NULL,";
               $sql_query .= sprintf(
                  "digest = \"%s\" where system = \"%s\";", $digest, $system);

               # Update the SQL db
               sqlInsert($sql_query);
            }

            logging("changed content: @changed");
            my $subject = sprintf("configuration changed on %s", $system);
            email($contact, $subject, @changed);
         }
      }
   }
   else {
      fatal(1, "cannot find the backup config, or backup config size is zero");
   }
}

# How many concurrent tasks
sub concurrent_tasks () { return $taskMax; }

# function, that start tasks
sub taskStart {
   my ($kernel, $heap) = @_[KERNEL, HEAP];
   while (keys(%{$heap->{task}}) < concurrent_tasks) {
      my $sql_query = "select active, system, snmp_id, ssh_id, ";
      $sql_query .= "tftp_server, system_contact from config_backup;";
      my @values = sqlSelect($sql_query);

      logging("how many systems: @values");
      my $system = shift @values;
      last unless defined $system;

      my $task = POE::Wheel::Run->new( 
         Program => sub {backup(@$system);}, 
         StdoutFilter => POE::Filter::Reference->new(),
      );

      $heap->{task}->{$task->ID} = $task;
      $kernel->sig_child($task->PID, "sig_child");
   }
}

# Once the task is done, delete the child wheel, and try to start a new
# task to take its place
sub taskDone {
   my ($kernel, $heap, $taskId) = @_[KERNEL, HEAP, ARG0];
   delete $heap->{task}->{$taskId};
   $kernel->yield("next_task");
}

# Detect the CHLD signal as each of our children exits.
sub sig_child {
   my ($heap, $signal, $pid, $exitValue) = @_[HEAP, ARG0, ARG1, ARG2];
   my $details = delete $heap->{$pid};
}

# main function, that backs up the config
sub backup { 
   my ($active, $system, $snmpId, $sshId, $tftp, $contact) = @_; 
   my $filter = POE::Filter::Reference->new();

   # If tftp is not defined, exit, we rely on TFTP for backups  
   if (!defined($tftp)) { 
      fatal(1, "cannot proceed without tftp server defined!"); 
   } 

   logging("TFTP server: $tftp");
   logging("system contact: $contact");

   # check to see whether the systems is active, means  whether we need to back 
   # it up or not!  
   if ($active eq 1) { 

      logging("device: $system");

      # All system require SNMP to be active.  This is the index into the 
      # snmp_table, without this index, the script will be unable to proceed 
      # as we backup all Cisco IOS devices using snmp and differentiate 
      # between IOS and ASA/PIX using sysDescr.0 snmpget 
      if ($snmpId eq 0) { 
         fatal(1, "cannot determine SNMP information for $system"); 
      } 

      logging("snmp table id: $snmpId");

      # Since we got the snmpId from the config_backup table,
      # get the snmp values from snmp_info table 
      my $sql_query = sprintf(
         "select * from snmp_info where id = %d;", $snmpId); 
      my @snmp = sqlSelect($sql_query); 
      my ($id, $ver, $ro, $rw, $user, $engine, $auth, $pass, $time) = $snmp[0]; 

      # Right now this script only cares about version 2/1 even though the 
      # underlying module (snmp.pm) supports it, this script does not yet!  
      # Create the ConfigCopy object, this is used to backup both the IOS and
      # ASA/PIX based devices.
      my $ccopy = new ConfigCopy; 
      my $retval = $ccopy->set_params($system, $rw, undef, $tftp, $sleep); 
      if ($retval ne 0) { 
         fatal(1, $ccopy->get_error()); 
      }

      logging("ssh table id: $sshId");
      # If we do need to back it up, see whether it is using ssh or not, usually
      # SSH means we are backing up an ASA or PIX, but is not a hard and fast
      # rule
      if ($sshId ne 0) { 

         # Same as SNMP, since we get the sshId, which is an index to the 
         # ssh_info table, get all the values from ssh_info for this matching id
         $sql_query = sprintf("select * from ssh_info where id = %d;", $sshId); 
         my @ssh = sqlSelect($sql_query); 

         # Even though we have the enable password, we are not using it yet!
         my ($id, $user, $passwd, $port, $enable, $time) = $ssh[0]; 

         logging("SSH username: $user");

         # This is where we check to see whether username and password are 
         # defined otherwise there is no point in progressing
         if (!defined($user)) { 
            $ccopy->close(); 
            fatal(1, "no user info for $system to connect over ssh"); 
         }

         if (!defined($passwd)) { 
            $ccopy->close(); 
            fatal(1, "no passwd info for $system to connect over ssh");
         }

         # Yeah, get the snmp description
         my $descr = $ccopy->get_device();

         logging("system description: $descr");
         
         # Proceed only if it is an ASA/PIX
         if (($descr == "asa") or ($descr == "pix")) { 

            if ($ccopy->ssh_connect(
                  $system, $port, $user, $passwd, undef) ne 0) {
               $ccopy->close(); 
               fatal(1, $ccopy->get_error()); 
            }
         }
      }

      if ($ccopy->get_config() eq 1) { 
         $ccopy->close(); 
         fatal(1, $ccopy->get_error());
      }

      my $config_file = $ccopy->get_config_file();

      logging("config file: $config_file");

      # If config file is defined,
      if (defined($config_file)) { 
         # Do RCS based backup, using Rcs module 
         rcs_backup($system, $contact, $config_file);
      }

      # close the config copy module
      $ccopy->close();
   }
   else {
      logging("not backing up $system, as it it not active");
   }

   exit(0);
}

# Check to see whether we have arguments
if ($arg) {
   # Use UNIX socket, bye bye windows
   setlogsock("unix");
   openlog("$program $$", "cons,pid", "user");

   # parse 'em all
   GetOptions(\%opt, 
      "debug|d", "conf|C=s", "sleep|s=i", "task|t=i", "tftp|T=s", 
      "hostname|h=s", "community|c=s", "user|u=s", "pass|p=s", 
      "help|?") or usage();
   usage() if $opt{help};

   if ($opt{debug}) {
      $debug = $opt{debug};
      logging("debug enabled!");
   }

   if ($opt{sleep}) {
      $sleep = $opt{sleep};
   }
   else {
      $sleep = 60;
   }

   logging("sleep time: $sleep");

   if ($opt{task}) {
      $taskMax = $opt{task};
   }
   
   # If database option is defined!
   if ($opt{conf}) {

      logging("sysbackup.pl config file: $opt{conf}");

      # debug option is only honored in the conf section (if loop) not
      # in else loop
      # if ($debug eq 0) {
         # Need to work on this
         # Proc::Daemon::Init;
         # }

      my $database = undef;
      my $hostname = undef;
      my $username = undef;
      my $password = undef;

      # Line count
      my $count = 0;

      # Open file, otherwise fatal it,
      open (FH, $opt{conf}) or fatal(1, "cannot open $opt{conf}: $!");

      # simple parser with the following syntax
      # db ssndb
      # host 10.200.1.5
      # user nflanders
      # pass 23Silver$
      #
      while (<FH>) {
         $count++;
         # I dont care if we have white spaced empty line or
         # any line that starts with # sign
         next if /^\s*#/;
         next if /^\s*$/;
         chomp(my $line = $_);

         if ($line =~ /^\s*(db|host|user|pass)\s+(.*)\s*$/) {
            if ($1 eq "db") {
               chomp($database = $2);
            }
            elsif ($1 eq "host") {
               chomp($hostname = $2);
            }
            elsif ($1 eq "user") {
               chomp($username = $2);
            }
            elsif ($1 eq "pass") {
               chomp($password = $2);
            }
            else {
               close(FH);
               fatal(0, "unknown token $1 on line $count");
            }
         }
      }

      # close the handle, yay yay!
      close(FH);

      if (sqlOpen($database, $hostname, $username, $password) != 0) {
         fatal(0, "problem opening sql handle!");
      }

      # We only enable rcs handle, if we have enough arguments!
      # bindir, rcs directory (where RCS director will be located), workdir 
      # ( where files to be rcs'ed will be located)
      if (!defined($rcs)) {
         $rcs = new ConfigRCS($bindir, $backup_dir, $backup_dir);
      }

      POE::Session->create(
         inline_states => 
         {
            _start => \&taskStart,
            next_task => \&taskStart,
            task_done => \&taskDone,
            sig_child => \&sig_child,
         }
      );

      # Run until there are no more tasks;
      $poe_kernel->run();

      # Close the SQL handle
      sqlClose();
   }
   else {
      # my variable defines
      my $tftp = undef;
      my $host = undef;
      my $snmprw = undef;
      my $user = undef;
      my $passwd = undef;

      # See whether tftp option is defined
      if ($opt{tftp}) {
         $tftp = $opt{tftp};
      }

      logging("tftp server: $tftp");

      # host option defined?
      if ($opt{host}) {
         $host = $opt{host};
      }
      
      logging("device: $host");

      # SNMP rw community string?
      if ($opt{community}) {
         $snmprw = $opt{community};
      }

      # ssh (version 2) username
      if ($opt{user}) {
         $user = $opt{user};
      }

      logging("ssh user: $user");

      # ssh (version 2) password for the above username
      if ($opt{pass}) {
         $passwd = $opt{pass};
      }

      # Check to see atleast tftp, host and snmp rw option is specified
      # Otherwise we die silently
      if (defined($tftp) and defined($host) and defined($snmprw)) {
         # instantiate the class!
         my $ccopy = new ConfigCopy;

         # set the v2c snmp parameters
         my $retval = $ccopy->set_params($host, $snmprw, undef, $tftp, $sleep);
         if ($retval ne 0) {
            $ccopy->close();
            fatal(1, $ccopy->get_error());
         }

         # If usernamw/password option is then, then we automatically try to
         # use SSH2 option to retrieve the configuration, by logging in first.
         if (defined($user) and defined($passwd)) {
            if ($ccopy->ssh_connect($host, undef, $user, $passwd, undef) ne 0) {
               $ccopy->close();
               fatal(1, $ccopy->get_error());
            }
         }

         # Get the config
         if ($ccopy->get_config() eq 1) {
            $ccopy->close();
            fatal(1, $ccopy->get_error());
         }

         # What is my config file name?
         my $config = $ccopy->get_config_file();
         my $digest = sha($config);

         logging("config file: $config");
         if (defined($digest)) {
            logging("config file digest: $digest");
         }

         # Open the config file, print it and close
         open (FH, $config) or fatal(1, "cannot open $config: $!");
         while (<FH>) {
            chomp(my $line = $_);
            printf("%s\n", $line);
         }

         close(FH) or fatal(1, "cannot close $config");

         # Remove the temporarily created config file
         unlink($config);

         # Close the handle
         $ccopy->close();
      }
   }

   # close sys logging!
   closelog;
}
else {
   usage();
}
# End
